Een uitgebreide gids voor het 'infer'-sleutelwoord in TypeScript, met uitleg over het gebruik met conditionele types voor krachtige type-extractie en -manipulatie.
Beheersing van TypeScript Infer: Conditionele Type-extractie voor Geavanceerde Typemanipulatie
Het typesysteem van TypeScript is ongelooflijk krachtig, waardoor ontwikkelaars robuuste en onderhoudbare applicaties kunnen bouwen. Een van de belangrijkste features die deze kracht mogelijk maakt, is het infer
-sleutelwoord in combinatie met conditionele types. Deze combinatie biedt een mechanisme voor het extraheren van specifieke types uit complexe typestructuren. Deze blogpost gaat diep in op het infer
-sleutelwoord, legt de functionaliteit ervan uit en toont geavanceerde use-cases. We zullen praktische voorbeelden verkennen die van toepassing zijn op diverse softwareontwikkelingsscenario's, van API-interactie tot de manipulatie van complexe datastructuren.
Wat zijn Conditionele Types?
Voordat we dieper ingaan op infer
, laten we eerst de conditionele types kort herhalen. Conditionele types in TypeScript stellen u in staat een type te definiƫren op basis van een voorwaarde, vergelijkbaar met een ternaire operator in JavaScript. De basissyntaxis is:
T extends U ? X : Y
Dit leest als: "Als type T
toewijsbaar is aan type U
, dan is het type X
; anders is het type Y
."
Voorbeeld:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introductie van het infer
Sleutelwoord
Het infer
-sleutelwoord wordt gebruikt binnen de extends
-clausule van een conditioneel type om een typevariabele te declareren die kan worden afgeleid van het type dat wordt gecontroleerd. In essentie stelt het u in staat om een deel van een type te "vangen" voor later gebruik.
Basissyntaxis:
type MyType<T> = T extends (infer U) ? U : never;
In dit voorbeeld, als T
toewijsbaar is aan een bepaald type, zal TypeScript proberen het type van U
af te leiden. Als de afleiding succesvol is, zal het type U
zijn; anders zal het never
zijn.
Eenvoudige Voorbeelden van infer
1. Het Afleiden van het Returntype van een Functie
Een veelvoorkomend gebruik is het afleiden van het returntype van een functie:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
In dit voorbeeld neemt ReturnType<T>
een functietype T
als input. Het controleert of T
toewijsbaar is aan een functie die willekeurige argumenten accepteert en een waarde retourneert. Als dat zo is, leidt het het returntype af als R
en retourneert het. Anders retourneert het any
.
2. Het Afleiden van het Array-elementtype
Een ander nuttig scenario is het extraheren van het elementtype uit een array:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Hier controleert ArrayElementType<T>
of T
een array-type is. Als dat zo is, leidt het het elementtype af als U
en retourneert het. Zo niet, dan retourneert het never
.
Geavanceerde Use Cases van infer
1. Het Afleiden van Parameters van een Constructor
U kunt infer
gebruiken om de parametertypes van een constructor-functie te extraheren:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
In dit geval neemt ConstructorParameters<T>
een constructor-functietype T
. Het leidt de types van de constructorparameters af als P
en retourneert deze als een tuple.
2. Eigenschappen Extraheren uit Objecttypes
infer
kan ook worden gebruikt om specifieke eigenschappen uit objecttypes te extraheren met behulp van mapped types en conditionele types:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//Een interface die geografische coƶrdinaten voorstelt.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Hier creƫert PickByType<T, K, U>
een nieuw type dat alleen de eigenschappen van T
bevat (met sleutels in K
) waarvan de waarden toewijsbaar zijn aan type U
. Het mapped type itereert over de sleutels van T
, en het conditionele type filtert de sleutels eruit die niet overeenkomen met het opgegeven type.
3. Werken met Promises
U kunt het opgeloste type van een Promise
afleiden:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Het Awaited<T>
-type neemt een type T
, dat wordt verwacht een Promise te zijn. Het type leidt vervolgens het opgeloste type U
van de Promise af en retourneert dit. Als T
geen promise is, retourneert het T. Dit is een ingebouwd utility type in nieuwere versies van TypeScript.
4. Het Type van een Array van Promises Extraheren
Door Awaited
te combineren met array-type-inferentie kunt u het type afleiden dat wordt opgelost door een array van Promises. Dit is met name handig bij het omgaan met Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Dit voorbeeld definieert eerst twee asynchrone functies, getUSDRate
en getEURRate
, die het ophalen van wisselkoersen simuleren. Het utility type PromiseArrayReturnType
extraheert vervolgens het opgeloste type van elke Promise
in de array, wat resulteert in een tuple-type waarbij elk element het awaited type is van de corresponderende Promise.
Praktische Voorbeelden uit Verschillende Domeinen
1. E-commerce Applicatie
Denk aan een e-commerce applicatie waar u productdetails ophaalt van een API. U kunt infer
gebruiken om het type van de productdata te extraheren:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simuleer API-aanroep
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Voorbeeldproduct',
price: 29.99,
description: 'Een voorbeeldproduct',
imageUrl: 'https://example.com/image.jpg',
category: 'Elektronica',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Productnaam: ${product.name}`);
console.log(`Prijs: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
In dit voorbeeld definiƫren we een Product
-interface en een fetchProduct
-functie die productdetails van een API ophaalt. We gebruiken Awaited
en ReturnType
om het Product
-type te extraheren uit het returntype van de fetchProduct
-functie, waardoor we de displayProductDetails
-functie kunnen typechecken.
2. Internationalisatie (i18n)
Stel dat u een vertaalfunctie heeft die verschillende strings retourneert op basis van de locale. U kunt infer
gebruiken om het returntype van deze functie te extraheren voor typeveiligheid:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}! `,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}! `,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Hier wordt TranslationType
afgeleid als de Translations
-interface, wat ervoor zorgt dat de greetUser
-functie de juiste type-informatie heeft voor toegang tot vertaalde strings.
3. API Responsafhandeling
Bij het werken met API's kan de responsstructuur complex zijn. infer
kan helpen bij het extraheren van specifieke datatypes uit geneste API-responsen:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simuleer API-aanroep
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Naam: ${profile.firstName} ${profile.lastName}`);
console.log(`Land: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
In dit voorbeeld definiƫren we een ApiResponse
-interface en een UserData
-interface. We gebruiken infer
en type-indexering om UserProfileType
uit de API-respons te extraheren, wat ervoor zorgt dat de displayUserProfile
-functie het juiste type ontvangt.
Best Practices voor het Gebruik van infer
- Houd het Eenvoudig: Gebruik
infer
alleen wanneer nodig. Overmatig gebruik kan uw code moeilijker leesbaar en begrijpelijk maken. - Documenteer Uw Types: Voeg commentaar toe om uit te leggen wat uw conditionele types en
infer
-statements doen. - Test Uw Types: Gebruik de typecontrole van TypeScript om ervoor te zorgen dat uw types zich gedragen zoals verwacht.
- Houd Rekening met Prestaties: Complexe conditionele types kunnen soms de compilatietijd beĆÆnvloeden. Wees u bewust van de complexiteit van uw types.
- Gebruik Utility Types: TypeScript biedt verschillende ingebouwde utility types (bijv.
ReturnType
,Awaited
) die uw code kunnen vereenvoudigen en de noodzaak voor aangepasteinfer
-statements kunnen verminderen.
Veelvoorkomende Valkuilen
- Onjuiste Afleiding: Soms kan TypeScript een type afleiden dat niet is wat u verwacht. Controleer uw typedefinities en voorwaarden dubbel.
- Circulaire Afhankelijkheden: Wees voorzichtig bij het definiƫren van recursieve types met
infer
, omdat dit kan leiden tot circulaire afhankelijkheden en compilatiefouten. - Te Complexe Types: Vermijd het creƫren van te complexe conditionele types die moeilijk te begrijpen en te onderhouden zijn. Breek ze op in kleinere, beter beheersbare types.
Alternatieven voor infer
Hoewel infer
een krachtig hulpmiddel is, zijn er situaties waarin alternatieve benaderingen geschikter kunnen zijn:
- Type Assertions: In sommige gevallen kunt u type assertions gebruiken om expliciet het type van een waarde te specificeren in plaats van het af te leiden. Wees echter voorzichtig met type assertions, omdat ze de typecontrole kunnen omzeilen.
- Type Guards: Type guards kunnen worden gebruikt om het type van een waarde te verfijnen op basis van runtime-controles. Dit is handig wanneer u verschillende types moet afhandelen op basis van runtime-voorwaarden.
- Utility Types: TypeScript biedt een rijke set van utility types die veelvoorkomende type-manipulatietaken kunnen afhandelen zonder de noodzaak van aangepaste
infer
-statements.
Conclusie
Het infer
-sleutelwoord in TypeScript, in combinatie met conditionele types, ontsluit geavanceerde mogelijkheden voor typemanipulatie. Het stelt u in staat om specifieke types te extraheren uit complexe typestructuren, waardoor u robuustere, onderhoudbare en type-veilige code kunt schrijven. Van het afleiden van functie-returntypes tot het extraheren van eigenschappen uit objecttypes, de mogelijkheden zijn enorm. Door de principes en best practices die in deze gids worden uiteengezet te begrijpen, kunt u infer
volledig benutten en uw TypeScript-vaardigheden naar een hoger niveau tillen. Vergeet niet uw types te documenteren, ze grondig te testen en alternatieve benaderingen te overwegen wanneer dat gepast is. Het beheersen van infer
stelt u in staat om echt expressieve en krachtige TypeScript-code te schrijven, wat uiteindelijk leidt tot betere software.